服务架构设计:从纯前端到全栈
组件库做到第14章已经能发布npm了,但一个完整的AI聊天应用还需要服务端。这节讲部署方式的变迁,以及如何在现有Monorepo中扩展服务端架构——从"AI帮你规划"到"包真正落地"。
从 mock server 说起
第10章的 Playground 演示早已不是写死在页面上的测试数据——它背后有一个本地 mock server,用的是前端侧非常火的服务框架 Hono,既能响应普通 HTTP 请求,也能响应流式(SSE)请求。有了这个基础,把它"做文章"的空间就打开了:mock 剧本引擎已经证明 Hono 能跑通完整的前后端链路,下一步是把里面可复用的网关能力抽成独立的包。
部署方式的变迁:从采购服务器到按量计费
传统上,开发和部署一个完整项目要懂后端、懂运维:采购云服务器,在上面部署 Redis 缓存、MongoDB/MySQL 数据库等一整套中间件。这条路的成本曲线近年发生了三次位移:
- 轻量应用服务器:云厂商把"一台开箱即用的服务器"产品化,省去自己装系统配环境;
- 云函数 / Serverless(腾讯云 SCF 等):不用采购整台机器,代码按调用计费,没流量就没账单;
- 云原生 SaaS + 边缘计算:不再采购任何服务器,直接把代码推到 Cloudflare Workers 这类边缘平台,推送即部署,按量计费。
其中最有代表性的是 Supabase——把数据库(Postgres)、认证、存储、边缘函数等一整套中间件整合进一个环境,配合 React/Next.js 就能完成一个纯前端视角的全栈项目。这些背景知识可以用豆包 PC 版这类通用大模型问几轮,大概就能摸清技术脉络。
本章要做的不是"把项目部署到 Supabase",而是更根本的一步:给自己的组件库配一个服务端包。这样后续才能对接 Agent、MCP 这些上层能力。
为什么组件库需要服务端
@ai-chat/vue 是纯前端组件库,但它依赖 ChatAdapter 接口与后端通信。第10章设计了接口但没有实现——因为实现方式取决于后端架构。现在要补上这块:
- 前端组件库:
core、vue、markdown(已完成) - 服务端包:
agents(AI代理/模型适配)、tools(工具集成)、server(HTTP 服务)(本章新增)
前端通过 HTTP API 调用服务端,服务端调用 AI 模型。分层架构确保前后端解耦。
Monorepo 服务端包拆分
packages/
core/ # 前端核心逻辑(已有)
vue/ # Vue 组件(已有)
markdown/ # Markdown 渲染(已有)
agents/ # AI 代理(新增)
tools/ # 工具集成(新增)
server/ # HTTP 服务(新增)
text
三个新包的职责:
| 包 | 职责 | 依赖 |
|---|---|---|
@ai-chat/agents | 模型适配、多协议调用、流式响应 | 零外部依赖(仅 workspace 内 core) |
@ai-chat/tools | 搜索、数据库、API 调用等工具 | 零外部依赖 |
@ai-chat/server | HTTP 路由、中间件、认证 | 依赖 agents + tools |
关键设计原则:agents 和 tools 不依赖任何 Web 框架,保持纯逻辑。server 包负责 HTTP 层,可以随时换框架。
后续演进注记(16-01 重构时的变化,此处先记下):tools 包后来并入了 agents(模型适配层)与 server(HTTP 网关),工具的"实例"(天气、文档检索等具体工具)则放进 dev-server——原则是"机制进库、实例进服务"。包名也从
@ai-chat/*统一改为@toimc/*(本章代码示例保留当时的@ai-chat叙事,标注真实包名时用@toimc)。
@ai-chat/agents 包设计
agents 包是 AI 模型交互的核心。以真实仓库 packages/agents/src/types.ts 的最终形态为准:
// packages/agents/src/types.ts(真实源码节选)
export interface ModelConfig {
/** 对外模型 id:前端 model 参数与 /api/models 返回值 */
id: string
/** 协议适配器类型:'openai-compat' | 'anthropic'(自定义适配器经 registerAdapter 接入) */
provider: string
/** 上游真实模型名,如 'gpt-5-mini' / 'claude-sonnet-4-5' */
model: string
apiKey: string
/** 上游 API 根地址,缺省用协议默认值 */
baseURL?: string
/** /api/models 展示名,缺省用 id */
name?: string
description?: string
}
/** 模型适配器统一接口:一个实现适配一种上游协议 */
export interface IModelAdapter {
chat(request: ChatRequest): Promise<ChatResponse>
chatStream(request: ChatRequest): AsyncGenerator<StreamChunk>
}
typescript
两个值得注意的设计:
- apiKey 只存在于服务端内存,禁止进入
/api/models公开视图或任何日志——注册中心list()返回的是脱敏的ModelPublicInfo; - provider 是"协议"而不是"厂商":真实实现没有为每家厂商写一个适配器,而是收敛为
openai-compat(覆盖 OpenAI/DeepSeek/GLM/Kimi/Ollama 等一切兼容端点)和anthropic两种协议,其他厂商的模型大多走这两种协议之一;真正的扩展点是registerAdapter(id, adapter, info),03 节的 Mastra 就是从这个口接进来的。
模型适配器模式——统一接口适配不同上游:
// packages/agents/src/adapters/openai-compat.ts(示意,真实源码为完整实现)
export class OpenAICompatibleAdapter implements IModelAdapter {
async *chatStream(request: ChatRequest): AsyncGenerator<StreamChunk> {
const response = await fetch(`${this.baseURL}/chat/completions`, {
method: 'POST',
headers: {
Authorization: `Bearer ${this.config.apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ ...body, stream: true }),
signal: request.signal,
})
// SSE 逐帧解析 → StreamChunk(真实实现复用包内 parseSseStream 工具)
}
}
typescript
模型注册中心(真实源码):
// packages/agents/src/model-registry.ts(节选)
const PROVIDER_FACTORIES: Record<string, (config: ModelConfig) => IModelAdapter> = {
'openai-compat': (config) => new OpenAICompatibleAdapter(config),
anthropic: (config) => new AnthropicAdapter(config),
}
export class ModelRegistry {
private models = new Map<string, RegisteredModel>()
/** 注册内置协议模型,重复 id 覆盖,返回 this 支持链式调用 */
register(config: ModelConfig): this { /* ... */ }
/** 注册自定义适配器(mock 剧本引擎、Mastra Agent 都走这个口),重复 id 覆盖 */
registerAdapter(id: string, adapter: IModelAdapter, info?: AdapterInfo): this { /* ... */ }
/** 未注册 id 抛错(消息包含 id) */
get(id: string): RegisteredModel { /* ... */ }
/** 注册顺序的公开视图(脱敏:无 apiKey / model / baseURL),返回浅拷贝 */
list(): ModelPublicInfo[] { /* ... */ }
}
typescript
@ai-chat/tools 包设计
工具需要同时满足两个角色——给 AI 看的描述(让它知道什么时候调用)和给系统用的执行器:
export interface ToolDefinition {
name: string
description: string
parameters: Record<string, ToolParameter>
}
export interface ToolExecutor {
definition: ToolDefinition
execute(params: Record<string, unknown>): Promise<ToolResult>
}
typescript
工具注册中心用 Map 存储定义与执行器,register() 返回 this 支持链式注册,list() 给模型侧提供工具清单。这套定义在真实仓库 types.ts 中保留为占位字段(对齐本章教学),工具的真实执行体系后来迁移到了 Mastra 的 createTool + zod schema(见 02/03 节)。
package.json 配置
以真实仓库 agents 包为参照(16-01 定稿形态,./mastra 可选子路径是后续演进加上的):
{
"name": "@toimc/agents",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.mjs",
"require": "./dist/index.cjs"
}
},
"files": ["dist"],
"dependencies": { "@toimc/core": "workspace:*" },
"peerDependencies": { "@mastra/core": "^1.60.0" },
"peerDependenciesMeta": { "@mastra/core": { "optional": true } },
"sideEffects": false
}
json
模型 API 调用用原生 fetch,不引 SDK——agents 包唯一的运行时依赖是 workspace 内的 core(类型),@mastra/core 是可选 peer(不装也能跑纯 mock 模式)。这保持了最小的包体积和最大的兼容性。
AI 辅助架构设计:一次真实的交互过程
给 Claude Code 一条指令让它规划服务端架构(不想来回讨论几轮的话,课程课件里附了已经规划好的完整指令):
> 我要给 ai-chat-ui 项目添加服务端能力。
> 需求:
> 1. 支持多模型(OpenAI/Claude/DeepSeek/本地 Ollama)
> 2. 支持工具调用(搜索/数据库查询)
> 3. 流式响应
>
> 请规划 packages 下的服务端架构,把 mock-server 里的核心 Hono 服务
> 封装成独立的包供发包使用。
text
交互过程中 AI 会逐项确认,两个关键决策点:
- 网关层的定位。AI 第一个方案就考虑得很周全:网关层只做接收和转发——线上 Serverless Worker 没有本地存储,服务发布上去只是单进程运行,要把用户侧数据收集下来必须对接中间件(数据库)。这决定了 server 包是"薄网关 + 可插拔存储"的形态。
- 协议矩阵与 mock 分包。AI 列出的上游选项涵盖云服务(OpenAI/Anthropic/DeepSeek)和本地推理(Ollama),选择"主流协议 + mock 部分单独成包"的方案后,它把目标拆解为:封装一个工厂函数、底层模块双导出(导出
createChatGateway工厂 + 导出装配好的默认实例),mock 侧直接调用工厂传入自己的剧本适配器。
架构审查没问题后直接让它开干。改造完成、代码推送后,文档站的"指南"部分会出现"服务端网关"一节——包含服务端接入、多模型注册(可注册多个模型与自定义适配器)、中间件、协议配置的说明。
Provider 生态:轮子已经造好了
各家模型协议的兼容库,官方基本都有自己的方案(openai-node、@anthropic-ai/sdk 等 Node 侧方案,Python 侧同理)。前端阵营里最省事的是 Vercel AI SDK:@ai-sdk/openai、@ai-sdk/anthropic、@ai-sdk/google……你需要哪一家就装哪一个包,对应模型的调用立即打通。Mastra 的 model router 也是同样的思路(一套统一路由覆盖主流 provider)。
但这不是本课程的重点——重点是我们自己造一个 provider 层。即使没有 Vercel AI SDK,也可以自己创建一个包,在里面对接各家的协议,甚至在这些协议之上做扩展和定制。理解了适配器模式,再看这些库就是"帮你写好了 N 个 Adapter 的合集"。
真实仓库的落地结果
最终落地(对照根目录 ai-chat-ui)的包结构:
packages/
agents/ # @toimc/agents:IModelAdapter + ModelRegistry + 内置两协议适配器
server/ # @toimc/server:createChatGateway(Hono 网关工厂)
dev-server/ # @toimc/dev-server:私有 dev 服务(8787),mock 剧本 + agents 装配
text
dev-server 入口的装配关系(真实源码简化):
// packages/dev-server/src/index.ts(简化)
import { serve } from '@hono/node-server'
import { createDevApp } from './app' // 内部调用 @toimc/server 的 createChatGateway
const app = await createDevApp()
serve({ fetch: app.fetch, port: 8787 })
typescript
包之间用 workspace:* 互相引用(pnpm workspace 的本地包协议,发布时自动替换版本号——14-03 讲过),dev-server 的 package.json 里同时挂着 @toimc/agents、@toimc/core、@toimc/server 与 hono。整个链路:dev-server(装配层)→ server(网关)→ agents(适配层)→ 上游模型,每层职责单一,换任何一层都不动其他层。
小结
- 部署方式三代变迁:轻量应用服务器 → 云函数/Serverless → SaaS + 边缘计算(Supabase 一站式整合),验证想法的成本已降到接近零
- Monorepo 新增服务端包:agents(模型适配)、tools(工具集成,后并入 agents/server)、server(HTTP 网关)
- agents 零外部依赖,原生 fetch;apiKey 只留在服务端内存,注册中心对外提供脱敏视图
- provider 收敛为协议适配(openai-compat / anthropic)+ registerAdapter 扩展点,而非每家厂商一个包
- AI 辅助架构设计的两个关键决策:网关只做接收转发(存储对接中间件)、mock 单独分包(工厂函数双导出)
下一节讲 Agents 核心概念——模型、工具、调度策略的分层设计。
↑